目標:想要完成一個有Onboarding 頁,簡述此App 功能及介紹,之後進入登入頁面,並包含有註冊功能,主題為旅遊的一個 App
下面先簡單介紹幾個今天會用到的 Widget內容,之後會在我們的範例專案上使用:
一個頁面,在Flutter裡面,被理解為一個Route
,多個route,可以存在與同一個dart文件中,可以通過Navigator
實現頁面之間的跳轉
第一頁的route 通常是 /
;如果是 /
,initialRoute 則不需要另外設定,因為 Flutter 會自動尋找 /
當作 home
若home 與 initialRoute 同時存在,優先去 initialRoute 頁面,若 initialRoute 為一個不存在的頁面,會去 home 頁
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SecondWidget(),
initialRoute: '/third',
routes: {
'/first': (_) =>FirstWidget(),
'/second':(_) =>SecondWidget(),
'/third':(_) =>ThirdWidget(),
},
);
}
}
//MyApp() 會先開啟ThirdWidget 頁
Navigator 可以先理解成 :
pop:回到上一頁
push:進入下一頁
範例:
Navigator.pushNamed(context, '/first');
//去 route 為/first 的頁面
現在手機有各式各樣的螢幕,像是瀏海等等,這時候我們在設計畫面時,可能會發生一些畫面被遮住的情況,此時SafeArea
就能幫我們解決問題,SafeArea
通過MediaQuery
來檢測螢幕尺寸,使應用程式的大小能與螢幕做調配
常用的一個控制項,設置具體尺寸
SizedBox
佈局行為相對較簡單:
child 不為null時,如果設置了寬高,則會強制把child尺寸調到此寬高;如果沒有設置寬高,則會根據child尺寸進行調整
child 為null時,如果設置了寬高,則自身尺寸調整到此寬高值,如果沒設置,則尺寸為0
Row:水平展示多個子控制元件的Widget
Column:垂直展示多個子控制元件的Widget
對齊方式:
主軸方向的對齊方式 mainAxisAlignment
MainAxis是主軸,就是與當前控制元件方向平行,Row 主軸為水平,Column 主軸為垂直
MainAxisAlignment.start:將子控制元件放在最靠近主軸起點的位置
MainAxisAlignment.end:將子控制元件放在最靠近主軸末端的位置
MainAxisAlignment.center:將子控制元件放在最靠近主軸中間的位置
MainAxisAlignment.spaceBetween:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離是0
MainAxisAlignment.spaceAround:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離為子控制元件距離的一半,即d/2
MainAxisAlignment.spaceEvenly:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離也是d
副軸方向的對齊方式 crossAxisAlignment
CrossAxis是交叉軸,就是與當前控制元件方向垂直的軸,Row 副軸為垂直,Column 副軸為水平
CrossAxisAlignment.start:將子控制元件的起始邊與crossAxis的起始邊對齊
CrossAxisAlignment.end:將子控制元件對齊於crossAxis的末端
CrossAxisAlignment.center:將子控制元件的起始邊與crossAxis的中間對齊
CrossAxisAlignment.stretch:子控制元件延伸至佔滿crossAxis
CrossAxisAlignment.baseline:將放置在螢幕上的子控制元件,對齊 baseLine (必須將TextBaseline Class與CrossAxisAlignment.baseline一起使用)
是用於擴展Row
、Column
以及Flex 的子控件,Expanded 會先讓同級的其他 Widget 先佈局,之後剩餘的空間,Expanded 才會去佔用,並依Expanded 的flex 係數自動伸縮
可以實現在一個widget 裡做頁面切換的效果,我們主要以PageView.builder 介绍一下PageView 的使用
PageView.builder 透過建構參數設定畫面:
itemBuilder
:每個頁面顯示的widgetitemCount
:頁面數量onPageChanged
:頁面切換會觸發的事件,可在此時更新現在位於哪個頁面controller
:控制器,例如:控制跳至某一頁、控制PageView初始頁面等為一個Widget元件並可以在尺寸、位置、透明度等方面,實現一些動畫效果
對AnimatedContainer 想要做動畫的屬性做狀態條件判斷來設定起始樣式和結束的樣式,並設定動畫時間,當AnimatedContainer 狀態條件改變時、自身屬性變化時,會自動切換樣式並計算顯示起始與結束之間的過渡動畫
RaisedButton
:有陰影,圓角FlatButton
:沒有陰影 沒有圓角 沒有邊框 ,背景透明OutlineButton
:沒有陰影 , 有圓角邊框IconButton
:有Icon 的按鈕按鈕屬性:onPressed,點擊按鈕的事件
在flutter中,color使用的是ARGB,0x後面的就是ARGB,A就是FF表示透明度,RGB就是三原色了
例如:Flutter 的logo,為完全不透明,red部分值是0x42 (66),gree部分值是0xA5 (165),blue部分值是0xF5 (245),在顏色值常用的hash syntax 語法中可寫為 #42A5F5
範例:
Color c = const Color(0xFF42A5F5); //16進位的ARGB
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5);
Color c = const Color.fromARGB(255, 66, 165, 245);
Color c = const Color.fromRGBO(66, 165, 245, 1.0); //opacity:不透明度
如果在渲染時沒有顏色出來,檢查一下顏色色碼值是8位16進制還是6位16進制,如果是6位的16進制,會預設在前面補兩個0,這樣這個顏色會是完全透明
Color c1 = const Color(0xFFFFFF); // fully transparent white (invisible) 完全透明
Color c2 = const Color(0xFFFFFFFF); // fully opaque white (visible) 完全不透明
Onboarding 頁構想:有三個分頁的畫面並可以左右滑動來敘述我們app 的特色,下方皆有跳過按鈕,讓使用者可以直接去登入頁
首先我們創建一個新的專案,我們去圖片下載,載三個圖片供我們onBoarding 的分頁用,在專案上新建一個資料夾assets,未來我們專案要使用到的資源就存在裡面,然後再在assets裡新建一個資料夾images 來給我們放圖片,之後我們把剛剛載的圖命名成splash_1、2、3,並在pubspec內去配置
配置 assets/images/ 未指定圖片的話,即該路徑的圖片皆可使用
我們先規劃onBoarding 的畫面,在 lib
資料夾下建立screens
資料夾,用來放之後要做的畫面,再在screens
下建立splash
資料夾,當作我們放onBoarding 畫面的地方,建一個splash_screen.dart
用來設計我們的onBoarding 畫面,再在splash
下建立components
資料夾,當作我們放onBoarding 畫面裡元件的地方,我們建一個body.dart
來處理我們onBoarding 畫面的body、splash_content.dart
來處理我們PageView.builder 每個頁面要顯示的widget
接著我們來建立一些共用的資料,共用的方法,像是app 的主要顏色,尺寸大小轉換等等,在 lib
資料夾下建立components
資料夾,用來放共用元件的widget,我們建一個default_button.dart
來設計共用的按鈕widget,之後我們回到lib
資料夾下,並建立constants.dart
來放一些共用的屬性( app 主要顏色、文字顏色 )、routes.dart
來放我們之後專案內所有頁面的route
、size_config.dart
來處理我們尺寸轉換等方法
constants.dart
:
import 'package:flutter/material.dart';
const kPrimaryColor = Color(0xFF3E4067);
const kPrimaryLightColor = Color(0xFF3E5067);
const kTextColor = Color(0xFF757575);
const kAnimationDuration = Duration(milliseconds: 200);
size_config.dart
:
import 'package:flutter/material.dart';
class SizeConfig {
static MediaQueryData _mediaQueryData;
static double screenWidth;
static double screenHeight;
static double defaultSize;
static Orientation orientation;
void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
orientation = _mediaQueryData.orientation;
}
}
// Get the proportionate height as per screen size
double getProportionateScreenHeight(double inputHeight) {
double screenHeight = SizeConfig.screenHeight;
// 815 is the layout height that designer use
return (inputHeight / 815.0) * screenHeight;
}
// Get the proportionate height as per screen size
double getProportionateScreenWidth(double inputWidth) {
double screenWidth = SizeConfig.screenWidth;
// 414 is the layout width that designer use
return (inputWidth / 414.0) * screenWidth;
}
// For add free space vertically (間距)
class VerticalSpacing extends StatelessWidget {
const VerticalSpacing({
Key key,
this.of = 25,
}) : super(key: key);
final double of;
@override
Widget build(BuildContext context) {
return SizedBox(
height: getProportionateScreenHeight(of),
);
}
}
default_button.dart
:
import 'package:flutter/material.dart';
import '../constants.dart';
import '../size_config.dart';
class DefaultButton extends StatelessWidget {
const DefaultButton({ // button onPressed 的方法透過建構傳入
Key key,
this.text,
this.press,
}) : super(key: key);
final String text;
final Function press;
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity, //to be as big as my parent allows (double.infinity)
height: getProportionateScreenHeight(56),
child: FlatButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), //圓角
color: kPrimaryColor,
onPressed: press,
child: Text(
text,
style: TextStyle(
fontSize: getProportionateScreenWidth(18),
color: Colors.white,
),
),
),
);
}
}
splash_content.dart
:
import 'package:flutter/material.dart';
import '../../../constants.dart';
import '../../../size_config.dart';
class SplashContent extends StatelessWidget {
const SplashContent({
Key key,
this.text,
this.image,
}) : super(key: key);
final String text, image;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
VerticalSpacing(of: 25),
Text(
"Travel Note",
style: TextStyle(
fontSize: getProportionateScreenWidth(36),
color: kPrimaryColor,
fontWeight: FontWeight.bold,
),
),
VerticalSpacing(of: 16),
Padding(
padding:
EdgeInsets.symmetric(horizontal: getProportionateScreenWidth(20)),
child: Text(
text,
textAlign: TextAlign.left,
style: TextStyle(
color: kTextColor,
height: 1.5,
fontSize: getProportionateScreenWidth(16),
),
),
),
VerticalSpacing(of: 40),
Image.asset(
image,
height: getProportionateScreenHeight(400),
width: double.infinity,
),
],
);
}
}
body.dart
:
import 'package:flutter/material.dart';
import 'package:travel_note/components/default_button.dart';
import 'package:travel_note/screens/login/login_screen.dart';
import 'package:travel_note/screens/splash/components/splash_content.dart';
import '../../../constants.dart';
import '../../../size_config.dart';
class Body extends StatefulWidget {
@override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> {
int currentPage = 0;
List<Map<String, String>> splashData = [
{
"text": "Welcome to Travel Note, Let’s plan a travel!",
"image": "assets/images/splash_1.jpg"
},
{
"text": "We show the easy way to plan travel.",
"image": "assets/images/splash_2.jpg"
},
{
"text": "Just start traveling with us!",
"image": "assets/images/splash_3.jpg"
},
];
@override
Widget build(BuildContext context) {
return SafeArea(
child: SizedBox(
width: double.infinity,
child: Column(
children: <Widget>[
Expanded(
child: PageView.builder(
onPageChanged: (value) {
setState(() {
currentPage = value;
});
},
itemCount: splashData.length,
itemBuilder: (context, index) => SplashContent(
image: splashData[index]["image"],
text: splashData[index]['text'],
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(
getProportionateScreenWidth(25),
getProportionateScreenWidth(25),
getProportionateScreenWidth(25),
getProportionateScreenWidth(40)),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
splashData.length,
(index) => buildDot(index: index),
),
),
VerticalSpacing(of: 40),
DefaultButton(
text: getButtonText(),
press: () {
Navigator.pushNamed(context, LoginScreen.routeName);
},
),
],
),
),
],
),
),
);
}
AnimatedContainer buildDot({int index}) {
return AnimatedContainer(
duration: kAnimationDuration,
margin: EdgeInsets.only(right: 5),
height: 6,
width: currentPage == index ? 20 : 6,
decoration: BoxDecoration(
color: currentPage == index ? kPrimaryColor : Color(0xFFD8D8D8),
borderRadius: BorderRadius.circular(3),
),
);
}
String getButtonText() {
if (currentPage == splashData.length - 1) {
return "Continue";
} else {
return "Skip";
}
}
}
splash_screen.dart
:
import 'package:flutter/material.dart';
import '../../size_config.dart';
import 'components/body.dart';
class SplashScreen extends StatelessWidget {
static String routeName = "/splash"; //設定onBoarding 頁的routeName
@override
Widget build(BuildContext context) {
// You have to call it on your starting screen
SizeConfig().init(context);
return Scaffold(
body: Body(), //使用body.dart 的Body()
);
}
}
routes.dart
:
import 'package:flutter/material.dart';
import 'package:travel_note/screens/splash/splash_screen.dart';
final Map<String, WidgetBuilder> routes = {
SplashScreen.routeName: (context) => SplashScreen() //route 為onBoarding,就開啟onBoarding 頁
};
main.dart
:
import 'package:flutter/material.dart';
import 'package:travel_note/routes.dart';
import 'package:travel_note/screens/splash/splash_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//拿掉畫面右上角的debug
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
/*
當底下的頁面有很多的時候,需要在 MaterialApp 中定義Routes 並且
同時設定 initialRoute,這樣進入 App 的時候,就會先進入 initRoutes,
再利用 Navigator 切換不同的頁面(Route)
initialRoute 是啓動APP的初始頁面,也就是用戶看到的第一個頁面
*/
initialRoute: SplashScreen.routeName,
routes: routes,
);
}
}
成果:
一開始設計畫面慢慢來沒關係,之後會慢慢發現設計畫面的動作都很像,並且我們越做越多共用的widget,記得自己動手做,很快就會熟練的,下一篇我們來處理登入頁